(function () {
  'use strict';

  var time = Date.now || function () {
    return +new Date();
  };

  var running = {};
  var counter = 1;
  var desiredFrames = 60;
  var millisecondsPerSecond = 100;

  var Animate = {

    // A requestAnimationFrame wrapper / polyfill.
    requestAnimationFrame: function () {
      var requestFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame;
      return function (callback, root) {
        requestFrame(callback, root);
      };
    }(),

    // Stops the given animation.
    stop: function stop(id) {
      var cleared = running[id] != null;
      if (cleared) {
        running[id] = null;
      }
      return cleared;
    },

    // Whether the given animation is still running.
    isRunning: function isRunning(id) {
      return running[id] != null;
    },

    // Start the animation.
    start: function start(stepCallback, verifyCallback, completedCallback, duration, easingMethod, root) {
      var _this = this;
      var start = time();
      var lastFrame = start;
      var percent = 0;
      var dropCounter = 0;
      var id = counter++;

      if (!root) {
        root = document.body;
      }

      // Compacting running db automatically every few new animations
      if (id % 20 === 0) {
        var newRunning = {};
        for (var usedId in running) {
          newRunning[usedId] = true;
        }
        running = newRunning;
      }

      // This is the internal step method which is called every few milliseconds
      var step = function step(virtual) {
        // Normalize virtual value
        var render = virtual !== true;
        // Get current time
        var now = time();

        // Verification is executed before next animation step
        if (!running[id] || verifyCallback && !verifyCallback(id)) {
          running[id] = null;
          completedCallback && completedCallback(desiredFrames - dropCounter / ((now - start) / millisecondsPerSecond), id, false);
          return;
        }

        // For the current rendering to apply let's update omitted steps in memory.
        // This is important to bring internal state variables up-to-date with progress in time.
        if (render) {
          var droppedFrames = Math.round((now - lastFrame) / (millisecondsPerSecond / desiredFrames)) - 1;
          for (var j = 0; j < Math.min(droppedFrames, 4); j++) {
            step(true);
            dropCounter++;
          }
        }

        // Compute percent value
        if (duration) {
          percent = (now - start) / duration;
          if (percent > 1) {
            percent = 1;
          }
        }

        // Execute step callback, then...
        var value = easingMethod ? easingMethod(percent) : percent;
        if ((stepCallback(value, now, render) === false || percent === 1) && render) {
          running[id] = null;
          completedCallback && completedCallback(desiredFrames - dropCounter / ((now - start) / millisecondsPerSecond), id, percent === 1 || duration == null);
        } else if (render) {
          lastFrame = now;
          _this.requestAnimationFrame(step, root);
        }
      };

      // Mark as running
      running[id] = true;
      // Init first step
      _this.requestAnimationFrame(step, root);
      // Return unique animation ID
      return id;
    }
  };

  function getElement(expr) {
    return typeof expr === 'string' ? document.querySelector(expr) : expr;
  }

  function getComputedStyle(el, key) {
    var computedStyle = window.getComputedStyle(el);
    return computedStyle[key] || '';
  }

  // Easing Equations (c) 2003 Robert Penner, all rights reserved.
  // Open source under the BSD License.
  function easeOutCubic(pos) {
    return Math.pow(pos - 1, 3) + 1;
  }

  function easeInOutCubic(pos) {
    if ((pos /= 0.5) < 1) {
      return 0.5 * Math.pow(pos, 3);
    }
    return 0.5 * (Math.pow(pos - 2, 3) + 2);
  }

  /*
   * Anima Scroller
   * Based Zynga Scroller (http://github.com/zynga/scroller)
   * Copyright 2011, Zynga Inc.
   * Licensed under the MIT License.
   * https://raw.github.com/zynga/scroller/master/MIT-LICENSE.txt
   */
  var TEMPLATE = '\n<div class="scroller-component" data-role="component">\n  <div class="scroller-mask" data-role="mask"></div>\n  <div class="scroller-indicator" data-role="indicator"></div>\n  <div class="scroller-content" data-role="content"></div>\n</div>\n';

  var Scroller = function Scroller(container, options) {
    var self = this;

    options = options || {};

    self.options = {
      itemClass: 'scroller-item',
      onSelect: function onSelect() {},
      defaultValue: 0,
      data: []
    };

    for (var key in options) {
      if (options[key] !== undefined) {
        self.options[key] = options[key];
      }
    }

    self.__container = getElement(container);

    var tempContainer = document.createElement('div');
    tempContainer.innerHTML = options.template || TEMPLATE;

    var component = self.__component = tempContainer.querySelector('[data-role=component]');
    var content = self.__content = component.querySelector('[data-role=content]');
    var indicator = component.querySelector('[data-role=indicator]');

    var data = self.options.data;
    var html = '';
    if (data.length && data[0].constructor === Object) {
      data.forEach(function (row) {
        html += '<div class="' + self.options.itemClass + '" data-value="' + row.value + '">' + row.name + '</div>';
      });
    } else {
      data.forEach(function (val) {
        html += '<div class="' + self.options.itemClass + '" data-value="' + val + '">' + val + '</div>';
      });
    }
    content.innerHTML = html;
    if (!self.__container) {
      return false;
    }
    self.__container.innerHTML = '';
    self.__container.appendChild(component);

    self.__itemHeight = parseInt(getComputedStyle(indicator, 'height'), 10);

    self.__callback = options.callback || function (top) {
      content.style.webkitTransform = 'translate3d(0, ' + -top + 'px, 0)';
    };

    var rect = component.getBoundingClientRect();

    self.__clientTop = rect.top + component.clientTop || 0;

    self.__setDimensions(component.clientHeight, content.offsetHeight);

    if (component.clientHeight === 0) {
      self.__setDimensions(parseInt(getComputedStyle(component, 'height'), 10), 204);
    }
    self.select(self.options.defaultValue, false);

    component.addEventListener('touchstart', function (e) {
      if (e.target.tagName.match(/input|textarea|select/i)) {
        return;
      }
      e.preventDefault();
      self.__doTouchStart(e.touches, e.timeStamp);
    }, false);

    component.addEventListener('touchmove', function (e) {
      self.__doTouchMove(e.touches, e.timeStamp);
    }, false);

    component.addEventListener('touchend', function (e) {
      self.__doTouchEnd(e.timeStamp);
    }, false);
  };

  var members = {
    value: null,
    __prevValue: null,
    __isSingleTouch: false,
    __isTracking: false,
    __didDecelerationComplete: false,
    __isGesturing: false,
    __isDragging: false,
    __isDecelerating: false,
    __isAnimating: false,
    __clientTop: 0,
    __clientHeight: 0,
    __contentHeight: 0,
    __itemHeight: 0,
    __scrollTop: 0,
    __minScrollTop: 0,
    __maxScrollTop: 0,
    __scheduledTop: 0,
    __lastTouchTop: null,
    __lastTouchMove: null,
    __positions: null,
    __minDecelerationScrollTop: null,
    __maxDecelerationScrollTop: null,
    __decelerationVelocityY: null,

    __setDimensions: function __setDimensions(clientHeight, contentHeight) {
      var self = this;

      self.__clientHeight = clientHeight;
      self.__contentHeight = contentHeight;

      var totalItemCount = self.options.data.length;
      var clientItemCount = Math.round(self.__clientHeight / self.__itemHeight);

      self.__minScrollTop = -self.__itemHeight * (clientItemCount / 2);
      self.__maxScrollTop = self.__minScrollTop + totalItemCount * self.__itemHeight - 0.1;
    },
    selectByIndex: function selectByIndex(index, animate) {
      var self = this;
      if (index < 0 || index > self.__content.childElementCount - 1) {
        return;
      }
      self.__scrollTop = self.__minScrollTop + index * self.__itemHeight;

      self.scrollTo(self.__scrollTop, animate);

      self.__selectItem(self.__content.children[index]);
    },
    select: function select(value, animate) {
      var self = this;

      var children = self.__content.children;
      for (var i = 0, len = children.length; i < len; i++) {
        if (children[i].dataset.value === value) {
          self.selectByIndex(i, animate);
          return;
        }
      }

      self.selectByIndex(0, animate);
    },
    getValue: function getValue() {
      return this.value;
    },
    scrollTo: function scrollTo(top, animate) {
      var self = this;

      animate = animate === undefined ? true : animate;

      if (self.__isDecelerating) {
        Animate.stop(self.__isDecelerating);
        self.__isDecelerating = false;
      }

      top = Math.round(top / self.__itemHeight) * self.__itemHeight;
      top = Math.max(Math.min(self.__maxScrollTop, top), self.__minScrollTop);

      if (top === self.__scrollTop || !animate) {
        self.__publish(top);
        self.__scrollingComplete();
        return;
      }
      self.__publish(top, 250);
    },
    destroy: function destroy() {
      this.__component.parentNode && this.__component.parentNode.removeChild(this.__component);
    },
    __selectItem: function __selectItem(selectedItem) {
      var self = this;

      var selectedItemClass = self.options.itemClass + '-selected';
      var lastSelectedElem = self.__content.querySelector('.' + selectedItemClass);
      if (lastSelectedElem) {
        lastSelectedElem.classList.remove(selectedItemClass);
      }
      selectedItem.classList.add(selectedItemClass);

      if (self.value !== null) {
        self.__prevValue = self.value;
      }

      self.value = selectedItem.dataset.value;
    },
    __scrollingComplete: function __scrollingComplete() {
      var self = this;

      var index = Math.round((self.__scrollTop - self.__minScrollTop - self.__itemHeight / 2) / self.__itemHeight);

      self.__selectItem(self.__content.children[index]);

      if (self.__prevValue !== null && self.__prevValue !== self.value) {
        self.options.onSelect(self.value);
      }
    },
    __doTouchStart: function __doTouchStart(touches, timeStamp) {
      var self = this;

      if (touches.length == null) {
        throw new Error('Invalid touch list: ' + touches);
      }
      if (timeStamp instanceof Date) {
        timeStamp = timeStamp.valueOf();
      }
      if (typeof timeStamp !== 'number') {
        throw new Error('Invalid timestamp value: ' + timeStamp);
      }

      self.__interruptedAnimation = true;

      if (self.__isDecelerating) {
        Animate.stop(self.__isDecelerating);
        self.__isDecelerating = false;
        self.__interruptedAnimation = true;
      }

      if (self.__isAnimating) {
        Animate.stop(self.__isAnimating);
        self.__isAnimating = false;
        self.__interruptedAnimation = true;
      }

      // Use center point when dealing with two fingers
      var currentTouchTop;
      var isSingleTouch = touches.length === 1;
      if (isSingleTouch) {
        currentTouchTop = touches[0].pageY;
      } else {
        currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2;
      }

      self.__initialTouchTop = currentTouchTop;
      self.__lastTouchTop = currentTouchTop;
      self.__lastTouchMove = timeStamp;
      self.__lastScale = 1;
      self.__enableScrollY = !isSingleTouch;
      self.__isTracking = true;
      self.__didDecelerationComplete = false;
      self.__isDragging = !isSingleTouch;
      self.__isSingleTouch = isSingleTouch;
      self.__positions = [];
    },
    __doTouchMove: function __doTouchMove(touches, timeStamp, scale) {
      var self = this;

      if (touches.length == null) {
        throw new Error('Invalid touch list: ' + touches);
      }
      if (timeStamp instanceof Date) {
        timeStamp = timeStamp.valueOf();
      }
      if (typeof timeStamp !== 'number') {
        throw new Error('Invalid timestamp value: ' + timeStamp);
      }

      // Ignore event when tracking is not enabled (event might be outside of element)
      if (!self.__isTracking) {
        return;
      }

      var currentTouchTop;

      // Compute move based around of center of fingers
      if (touches.length === 2) {
        currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2;
      } else {
        currentTouchTop = touches[0].pageY;
      }

      var positions = self.__positions;

      // Are we already is dragging mode?
      if (self.__isDragging) {
        var moveY = currentTouchTop - self.__lastTouchTop;
        var scrollTop = self.__scrollTop;

        if (self.__enableScrollY) {
          scrollTop -= moveY;

          var minScrollTop = self.__minScrollTop;
          var maxScrollTop = self.__maxScrollTop;

          if (scrollTop > maxScrollTop || scrollTop < minScrollTop) {
            // Slow down on the edges
            if (scrollTop > maxScrollTop) {
              scrollTop = maxScrollTop;
            } else {
              scrollTop = minScrollTop;
            }
          }
        }

        // Keep list from growing infinitely (holding min 10, max 20 measure points)
        if (positions.length > 40) {
          positions.splice(0, 20);
        }

        // Track scroll movement for decleration
        positions.push(scrollTop, timeStamp);

        // Sync scroll position
        self.__publish(scrollTop);

        // Otherwise figure out whether we are switching into dragging mode now.
      } else {
        var minimumTrackingForScroll = 0;
        var minimumTrackingForDrag = 5;

        var distanceY = Math.abs(currentTouchTop - self.__initialTouchTop);

        self.__enableScrollY = distanceY >= minimumTrackingForScroll;

        positions.push(self.__scrollTop, timeStamp);

        self.__isDragging = self.__enableScrollY && distanceY >= minimumTrackingForDrag;

        if (self.__isDragging) {
          self.__interruptedAnimation = false;
        }
      }

      // Update last touch positions and time stamp for next event
      self.__lastTouchTop = currentTouchTop;
      self.__lastTouchMove = timeStamp;
      self.__lastScale = scale;
    },
    __doTouchEnd: function __doTouchEnd(timeStamp) {
      var self = this;

      if (timeStamp instanceof Date) {
        timeStamp = timeStamp.valueOf();
      }
      if (typeof timeStamp !== 'number') {
        throw new Error('Invalid timestamp value: ' + timeStamp);
      }

      // Ignore event when tracking is not enabled (no touchstart event on element)
      // This is required as this listener ('touchmove') sits on the document and not on the element itself.
      if (!self.__isTracking) {
        return;
      }

      // Not touching anymore (when two finger hit the screen there are two touch end events)
      self.__isTracking = false;

      // Be sure to reset the dragging flag now. Here we also detect whether
      // the finger has moved fast enough to switch into a deceleration animation.
      if (self.__isDragging) {
        // Reset dragging flag
        self.__isDragging = false;

        // Start deceleration
        // Verify that the last move detected was in some relevant time frame
        if (self.__isSingleTouch && timeStamp - self.__lastTouchMove <= 100) {
          // Then figure out what the scroll position was about 100ms ago
          var positions = self.__positions;
          var endPos = positions.length - 1;
          var startPos = endPos;

          // Move pointer to position measured 100ms ago
          for (var i = endPos; i > 0 && positions[i] > self.__lastTouchMove - 100; i -= 2) {
            startPos = i;
          }

          // If start and stop position is identical in a 100ms timeframe,
          // we cannot compute any useful deceleration.
          if (startPos !== endPos) {
            // Compute relative movement between these two points
            var timeOffset = positions[endPos] - positions[startPos];
            var movedTop = self.__scrollTop - positions[startPos - 1];

            // Based on 50ms compute the movement to apply for each render step
            self.__decelerationVelocityY = movedTop / timeOffset * (1000 / 60);

            // How much velocity is required to start the deceleration
            var minVelocityToStartDeceleration = 4;

            // Verify that we have enough velocity to start deceleration
            if (Math.abs(self.__decelerationVelocityY) > minVelocityToStartDeceleration) {
              self.__startDeceleration(timeStamp);
            }
          }
        }
      }

      if (!self.__isDecelerating) {
        self.scrollTo(self.__scrollTop);
      }

      // Fully cleanup list
      self.__positions.length = 0;
    },

    // Applies the scroll position to the content element
    __publish: function __publish(top, animationDuration) {
      var self = this;

      // Remember whether we had an animation, then we try to continue based on the current "drive" of the animation
      var wasAnimating = self.__isAnimating;
      if (wasAnimating) {
        Animate.stop(wasAnimating);
        self.__isAnimating = false;
      }

      if (animationDuration) {
        // Keep scheduled positions for scrollBy functionality
        self.__scheduledTop = top;

        var oldTop = self.__scrollTop;
        var diffTop = top - oldTop;

        var step = function step(percent, now, render) {
          self.__scrollTop = oldTop + diffTop * percent;
          // Push values out
          if (self.__callback) {
            self.__callback(self.__scrollTop);
          }
        };

        var verify = function verify(id) {
          return self.__isAnimating === id;
        };

        var completed = function completed(renderedFramesPerSecond, animationId, wasFinished) {
          if (animationId === self.__isAnimating) {
            self.__isAnimating = false;
          }
          if (self.__didDecelerationComplete || wasFinished) {
            self.__scrollingComplete();
          }
        };

        // When continuing based on previous animation we choose an ease-out animation instead of ease-in-out
        self.__isAnimating = Animate.start(step, verify, completed, animationDuration, wasAnimating ? easeOutCubic : easeInOutCubic);
      } else {
        self.__scheduledTop = self.__scrollTop = top;
        // Push values out
        if (self.__callback) {
          self.__callback(top);
        }
      }
    },

    // Called when a touch sequence end and the speed of the finger was high enough to switch into deceleration mode.
    __startDeceleration: function __startDeceleration(timeStamp) {
      var self = this;

      self.__minDecelerationScrollTop = self.__minScrollTop;
      self.__maxDecelerationScrollTop = self.__maxScrollTop;

      // Wrap class method
      var step = function step(percent, now, render) {
        self.__stepThroughDeceleration(render);
      };

      // How much velocity is required to keep the deceleration running
      var minVelocityToKeepDecelerating = 0.5;

      // Detect whether it's still worth to continue animating steps
      // If we are already slow enough to not being user perceivable anymore, we stop the whole process here.
      var verify = function verify() {
        var shouldContinue = Math.abs(self.__decelerationVelocityY) >= minVelocityToKeepDecelerating;
        if (!shouldContinue) {
          self.__didDecelerationComplete = true;
        }
        return shouldContinue;
      };

      var completed = function completed(renderedFramesPerSecond, animationId, wasFinished) {
        self.__isDecelerating = false;
        if (self.__scrollTop <= self.__minScrollTop || self.__scrollTop >= self.__maxScrollTop) {
          self.scrollTo(self.__scrollTop);
          return;
        }
        if (self.__didDecelerationComplete) {
          self.__scrollingComplete();
        }
      };

      // Start animation and switch on flag
      self.__isDecelerating = Animate.start(step, verify, completed);
    },

    // Called on every step of the animation
    __stepThroughDeceleration: function __stepThroughDeceleration(render) {
      var self = this;

      var scrollTop = self.__scrollTop + self.__decelerationVelocityY;

      var scrollTopFixed = Math.max(Math.min(self.__maxDecelerationScrollTop, scrollTop), self.__minDecelerationScrollTop);
      if (scrollTopFixed !== scrollTop) {
        scrollTop = scrollTopFixed;
        self.__decelerationVelocityY = 0;
      }

      if (Math.abs(self.__decelerationVelocityY) <= 1) {
        if (Math.abs(scrollTop % self.__itemHeight) < 1) {
          self.__decelerationVelocityY = 0;
        }
      } else {
        self.__decelerationVelocityY *= 0.95;
      }

      self.__publish(scrollTop);
    }
  };

  // Copy over members to prototype
  for (var key in members) {
    Scroller.prototype[key] = members[key];
  }
  window.Scroller = Scroller;
  // module.exports = Scroller;
})();